﻿using LettuceEncrypt;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Quickly.Certs.EntityFrameworkCore;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Quickly.Certs.WebApi.Endpoints
{
    public static class DomainCertificateEndpoints
    {
        public static IEndpointRouteBuilder MapManagementEndpoints(this IEndpointRouteBuilder app)
        {
            var api = app.MapGroup("/api");

            api.MapGet("/domains", ListDomains).WithName("ListDomains");
            api.MapGet("/download-pfx", DownloadPfx).WithName("DownloadPfx");

            return app;
        }

        public static async Task<Ok<List<DomainDto>>> ListDomains(
            [FromServices] CertsContext context,
            [FromServices] IHttpContextAccessor accessor)
        {
            var query = from domain in context.DomainNames
                        join cert in context.CertificateFiles.Where(x => x.Deleted == false) on domain.CertificateId equals cert.Id into result
                        from right in result.DefaultIfEmpty()
                        select new DomainDto(domain.Name, right.Id, right.NotAfter);

            var list = await query.ToListAsync(accessor.HttpContext?.RequestAborted ?? CancellationToken.None);

            return TypedResults.Ok(list);
        }

        public static async Task<Results<FileContentHttpResult, NotFound>> DownloadPfx(
            [FromQuery] long id,
            [FromQuery] string downloadName,
            [FromServices] CertsContext context,
            [FromServices] IHttpContextAccessor accessor)
        {
            var data = await context.CertificateFiles
                .Where(x => x.Id == id && x.Deleted == false)
                .FirstOrDefaultAsync(accessor.HttpContext?.RequestAborted ?? CancellationToken.None);

            if (data == null)
            {
                return TypedResults.NotFound();
            }

            return TypedResults.File(data.Data, "application/octet-stream", downloadName + ".pfx");
        }

        public static async Task<Results<FileStreamHttpResult, NotFound>> DownloadKeyAndCrt(
            [FromQuery] long id,
            [FromQuery] string downloadName,
            [FromServices] IOptions<LettuceEncryptOptions> options,
            [FromServices] CertsContext context,
            [FromServices] IHttpContextAccessor accessor)
        {
            var data = await context.CertificateFiles
                .Where(x => x.Id == id && x.Deleted == false)
                .FirstOrDefaultAsync(accessor.HttpContext?.RequestAborted ?? CancellationToken.None);

            if (data == null)
            {
                return TypedResults.NotFound();
            }

            // pfx转换为crt+key
            var pfx = X509CertificateLoader.LoadPkcs12(data.Data, data.PfxPassword);

            FileStreamHttpResult? result = null;

            var ms = new MemoryStream();

            var certBytes = pfx.Export(X509ContentType.Cert);
            var certPem = pfx.ExportCertificatePem();

            var publicKeyBytes = pfx.GetPublicKey();
            string keyPem = string.Empty;
            byte[] keyBytes = [];
            var keySpans = new Span<char>();

            if (pfx.HasPrivateKey)
            {
                if(pfx.GetRSAPrivateKey() is RSA rsa)
                {
                    keyBytes = rsa.ExportPkcs8PrivateKey();
                    keyPem = rsa.ExportPkcs8PrivateKeyPem();
                }
                else if(pfx.GetECDsaPrivateKey() is ECDsa ecdsa)
                {
                    if(ecdsa.TryExportECPrivateKeyPem(keySpans, out var _) == false)
                    {
                        if(ecdsa.TryExportPkcs8PrivateKeyPem(keySpans, out var _) == false)
                        {
                            //if(ecdsa.TryExportEncryptedPkcs8PrivateKeyPem(data.PfxPassword, new PbeParameters(), keySpans, out var _) == false)
                            //{

                            //}

                            throw new InvalidOperationException("获取private key失败");
                        }
                    } 

                    keyBytes = ecdsa.ExportECPrivateKey();
                    keyPem = ecdsa.ExportECPrivateKeyPem();
                }
            }

            using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
            {
                var crtEntry = zip.CreateEntry("ca.crt");
                using (var crtStream = crtEntry.Open())
                {
                    crtStream.Write(certBytes, 0, certBytes.Length);
                }


                var keyEntry = zip.CreateEntry("private.key");
                using (var keyStream = keyEntry.Open())
                {
                    using (var sw = new StreamWriter(keyStream))
                    {
                        sw.WriteLine(keyPem);
                    }
                }
            }

            ms.Position = 0;

            result = TypedResults.File(ms, "application/octet-stream", downloadName + ".zip");

            if (result == null)
            {
                return TypedResults.NotFound();
            }

            return result;
        }

        public static string ToPem(byte[] data, string label)
        {
            const string pemHeader = "-----BEGIN {0}-----";
            const string pemFooter = "-----END {0}-----";

            string base64Data = Convert.ToBase64String(data);
            string formattedData = String.Empty;

            for (int i = 0; i < base64Data.Length; i += 64)
            {
                formattedData += base64Data.Substring(i, Math.Min(64, base64Data.Length - i)) + Environment.NewLine;
            }

            return string.Format(pemHeader, label) + Environment.NewLine +
                   formattedData +
                   string.Format(pemFooter, label) + Environment.NewLine;
        }
    }

    public record DomainDto(string Domain, long? CertificateId, DateTimeOffset? NotAfter);
}
